本日核心價值 (Core Focus): 用 n8n 或 Make 把「Cron → 呼叫 OpenAI → Slack/Email」做成可重跑的日常摘要工作流,密鑰放憑證存放區、每日 digest 做冪等,並保留一支 Python cron 作為不依賴 SaaS 的後備。
概念說明與實戰情境 (Overview)
Day 24 的 Git 腳本仍要人在終端機執行。營運向的工作——昨日 CI 失敗摘要、待辦票數、知識庫低分拒絕次數、成本 log 日切(Day 22)——適合排程,而不是開著筆電。n8n 與 Make.com 都提供 Cron、HTTP、Slack、Email 模組,本質相同:時間觸發、組 Prompt、打 OpenAI、把結構化結果送到頻道。差異在託管與計價,不在工作流形狀。上線前先定兩條規則:API Key 不寫進節點參數;同一天的 digest 重跑不得重複洗版。做不到這兩點,排程只是把帳單與通知事故自動化。
關鍵操作與範例 (Implementation & Example)
1. n8n 主路徑:Cron → HTTP OpenAI → Slack / Email
建議節點順序:
0 9 * * 1-5(週一到週五 09:00,時區設團隊所在地)。POST https://api.openai.com/v1/chat/completions(或團隊現用的 Responses 端點)。Authorization 來自 Credentials,不是節點裡的字串。no_changes: true,停止,不發 Slack。OpenAI 節點參數(高層示意,密鑰用 credential 參照):
{
"nodes": [
{
"name": "Weekday 09:00",
"type": "n8n-nodes-base.cron",
"parameters": {
"triggerTimes": {
"item": [{ "hour": 9, "minute": 0 }]
}
}
},
{
"name": "OpenAI Daily Digest",
"type": "n8n-nodes-base.httpRequest",
"parameters": {
"method": "POST",
"url": "https://api.openai.com/v1/chat/completions",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"jsonBody": {
"model": "gpt-4.1-mini",
"temperature": 0,
"messages": [
{
"role": "system",
"content": "Summarize ops JSON into Traditional Chinese bullet points. Return JSON {title, bullets, no_changes}."
},
{
"role": "user",
"content": "={{ JSON.stringify($json) }}"
}
]
}
}
},
{
"name": "Slack Digest",
"type": "n8n-nodes-base.slack",
"parameters": {
"channel": "#eng-digest",
"text": "={{ $json.choices[0].message.content }}"
}
}
]
}
實際 UI 裡 Header Auth 應綁「OpenAI API」credential;JSON 僅示意欄位名稱與資料流,貼上後仍需在編輯器對齊 n8n 版本的參數結構。Slack Bot Token 同樣走 Slack credential,不寫進 text 參數。OpenAI 節點應設 timeout(例如 30–60 秒)與有上限的重試:只對 429 / 5xx 重試,4xx 直接失敗並告警。Digest 的輸入 JSON 先由內部 API 聚合,模型只做「把已計算好的數字寫成可讀條列」,不要在排程裡臨時再打一遍所有 LLM 請求。
2. Make.com 對等模組
| n8n | Make.com | 注意 |
|---|---|---|
| Cron | Schedule / Watch | 時區與夏令時間;不要用瀏覽器本地時間假設 |
| HTTP Request → OpenAI | OpenAI / HTTP | 連線放 Connections,不放 module 欄位 |
IF no_changes |
Filter / Router | 無變更就結束 scenario |
| Slack | Slack > Create a Message | channel ID 用變數,勿寫死 webhook URL 在分享的 blueprint |
| Email / Gmail / Microsoft 365 | 收件人清單當團隊設定,不要讓模型指定任意外寄地址(Day 21 允許清單) |
Make 的 scenario 匯出若含連線,分享前先換成空 Connections。模型輸出若包含「請改寄到某某信箱」,應用 Filter 擋下;收件人只來自 Make 設定。工作流失敗要另開告警(Email 或 Pager),不要把錯誤堆在同一則 digest:digest 沒送到,值班會以為「今天沒事」。OpenAI 與 Slack 節點都失敗時,冪等鍵不可標記為已送,隔天重跑才補得回。
3. 每日 digest 的冪等
Cron 會重試、手動 Execute Workflow 會再跑一次。冪等鍵用日期(團隊時區的 YYYY-MM-DD)+ workflow 名稱。第一次成功寫入資料表或 n8n Static Data / Make Data Store:digest:2026-08-17=sent。後續執行先查鍵,已存在就結束。不要用「訊息內容相同就不送」當唯一條件:內容可能因模型非確定性而微變,導致重複通知。溫度設 0、仍以日期鍵為準。
若摘要來源是 GitHub 或內部 API,拉取時帶 since=yesterday 00:00&until=today 00:00 的閉開區間,避免重跑把同一筆失敗 job 算兩次。這與 Day 22 的 per-request cost log 銜接:digest 讀「昨日加總」,不現場再打一遍所有 LLM 請求。時區必須寫死為團隊時區(本系列預設 Asia/Taipei),不要用 runner 的本地時間;否則週一早上可能吃到週日 UTC 的半日資料,冪等鍵也會錯日。指標建議固定欄位:failed_jobs、input_tokens_sum、usd_estimate_sum、rag_refuse_count、open_tickets,缺值填 0,方便模型與 IF 節點穩定解析。
4. Python 後備:系統 cron / Task Scheduler
SaaS 配額用盡、n8n 升級失敗時,同一份契約應能在本機或既有 runner 跑。
from __future__ import annotations
import hashlib
import json
import os
from datetime import datetime, timedelta, timezone
from pathlib import Path
from openai import OpenAI
STATE = Path(os.environ.get("DIGEST_STATE", ".digest-state.json"))
TZ_OFFSET_HOURS = 8 # Asia/Taipei without extra tzdata dependency
def today_key() -> str:
local = datetime.now(timezone.utc) + timedelta(hours=TZ_OFFSET_HOURS)
return local.date().isoformat()
def already_sent(day: str) -> bool:
if not STATE.exists():
return False
data = json.loads(STATE.read_text(encoding="utf-8"))
return data.get("last_sent_day") == day
def mark_sent(day: str, fingerprint: str) -> None:
STATE.write_text(
json.dumps({"last_sent_day": day, "fingerprint": fingerprint}, ensure_ascii=False),
encoding="utf-8",
)
def load_metrics() -> dict:
path = Path(os.environ["METRICS_JSON"])
return json.loads(path.read_text(encoding="utf-8"))
def main() -> None:
day = today_key()
if already_sent(day):
print(f"idempotent skip: {day}")
return
metrics = load_metrics()
client = OpenAI()
resp = client.chat.completions.create(
model=os.environ.get("CHAT_MODEL", "gpt-4.1-mini"),
temperature=0,
messages=[
{
"role": "system",
"content": (
"Summarize ops metrics as Traditional Chinese bullets. "
"Return JSON with keys title, bullets, no_changes."
),
},
{"role": "user", "content": json.dumps(metrics, ensure_ascii=False)},
],
)
content = resp.choices[0].message.content or "{}"
parsed = json.loads(content)
if parsed.get("no_changes"):
mark_sent(day, "no_changes")
print("no changes; marked sent")
return
webhook = os.environ["SLACK_WEBHOOK_URL"]
import urllib.request
body = json.dumps({"text": parsed.get("title", "digest") + "\n" + "\n".join(parsed.get("bullets") or [])}).encode()
req = urllib.request.Request(webhook, data=body, headers={"Content-Type": "application/json"})
urllib.request.urlopen(req, timeout=30)
mark_sent(day, hashlib.sha256(content.encode()).hexdigest())
print(f"sent digest for {day}")
if __name__ == "__main__":
main()
Linux / WSL:crontab 寫 0 9 * * 1-5 cd /path && . .venv/bin/activate && python digest.py。Windows:工作排程器呼叫 WSL 或 Git Bash。OPENAI_API_KEY、SLACK_WEBHOOK_URL 放環境變數或 secret store,不放進 repo。Webhook 僅用於後備腳本的單一頻道;n8n 路徑優先用 Slack credential。後備腳本與 n8n 必須共用同一份冪等狀態(資料表或網路磁碟上的 state 檔),否則兩條排程會同一天各送一次。切換主路徑時先停掉另一條,再清當天鍵,避免補送變成重送。
注意事項與常見失敗 (Pitfalls)
本日總結 (Takeaways)
明日預告 (Next)
排程會把「慢」與「漏」變成每天的噪音。下一步回到執行期本身:Day 26 將做系統效能調優:利用 ChatGPT 快速發現 Memory Leak 與 I/O 瓶頸。